Principles of Programming Languages — Appunti TiTilda

Indice

Scheme

Scheme is a minimalist dialect of the Lisp programming language, designed with a focus on simplicity and flexibility.

Scheme is (mostly) a functional programming language: every computation is an expression that evaluates to a value, and there are no statements or commands.

Statements are instructions that perform actions but do not return values, while expressions are constructs that evaluate to produce values.

The syntax of Scheme is characterized by its use of parentheses to denote function application and its uniform treatment of code and data. This type of syntax is known as S-expressions (Symbolic Expressions) and uses prefix (or Polish) notation.

Each expression in Scheme is enclosed in parentheses ((operator arg1 ... argn)), where operator is typically a symbol representing a function or special form, and arg1 to argn are its arguments.

The evaluation order of arguments in an expression is unspecified, meaning that the language does not guarantee a specific order in which the arguments are evaluated, but it ensures that all arguments are evaluated before the procedure is applied.

For example, the mathematical expression 5 + 3 \cdot 2 + z is written in Scheme as:

(+ 5 (* 3 2) z)

It is also possible to use the square brackets [ and ] as parentheses for better readability, especially in nested expressions:

(+ 5 [* 3 2] z)

Scheme is homoiconic, meaning that the code and data share the same representation.

Homoiconicity: The property where the program structure is similar to its data structure. In Lisp dialects, both code and data are represented as S-expressions (nested lists).

In general, most programming languages are not homoiconic to separate code from data, making it easier to reason about programs.

This uniform syntax allows for powerful metaprogramming capabilities, as code can be manipulated as data structures.

Metaprogramming is the practice of writing programs that can generate, manipulate, or analyze other programs or themselves as data.

Variables and Bindings

The scope of variables in Scheme is static (or lexical), meaning that the visibility of a variable is determined by the structure of the code and the location where it is defined.

Scoping can be either static (lexical) or dynamic.

In static scoping, the scope of a variable is determined by the program’s structure (at compile time), while in dynamic scoping, the scope is determined by the program’s execution context (call stack at runtime).

Variable are stored in the heap memory area, which is used for dynamic memory allocation. The heap allows for the creation of variables whose lifetimes are not tied to the function call stack, enabling features like closures.

There is also a Garbage Collector (GC) in Scheme that automatically reclaims memory occupied by objects that are no longer reachable or needed by the program.

When a variable is defined as a constant the value is immutable and the memory area is marked as read-only to prevent accidental modification. This prevents banging operations.

Local Variables

Local variables in Scheme are created using the let keyword, which allows for the binding of values to names.

(let ((x 10)
      (y 20))
  (+ x y)) ; This will evaluate to 30

The let construct creates a new scope where x is bound to 10 and y is bound to 20.

The binding of multiple variable happens in parallel, meaning that the values of the variables cannot depend on each other, inside of the same let expression.

(let ((x 10)
      (y (+ x 5))) ; This will result in an error because x is not yet defined
  (+ x y))
Sequential Bindings

To create sequential bindings, where the value of a variable can depend on previously defined variables, the let* construct is used.

(let* ((x 10)
       (y (+ x 5))) ; This is valid because x is already defined
  (+ x y)) ; This will evaluate to 25
Recursive Bindings

For defining recursive functions or variables that refer to themselves, the letrec construct is used.

(letrec ((factorial
           (lambda (n)
             (if (= n 0)
                 1
                 (* n (factorial (- n 1)))))))
  (factorial 5)) ; This will evaluate to 120

Global Variables

Global variables in Scheme can be defined using the define keyword. These variables are accessible from any part of the program after their definition.

(define pi 3.14159)
(define radius 5)
(* pi (* radius radius)) ; This will evaluate to 78.53975

Sometimes global variables are defined surrounded by * symbols to indicate that they are special or global variables, for example (define *global-var* '()).

Variable Assignment

In Scheme, variables are immutable by default, meaning that once a variable is bound to a value, it cannot be changed.

However, mutable variables can be created using the set! keyword.

(define counter 0)
(set! counter (+ counter 1)) ; This will update the value of counter to 1

The ! operator is called a bang and is used to indicate that a function or operation has side effects, such as modifying a variable or changing the state of the program.

Types

Scheme is a dynamically typed language, meaning that types are associated with values rather than variables. However, it is also strongly typed, as it enforces type safety during runtime operations (e.g., trying to add a number to a string will result in an error).

Scheme supports a variety of data types, including:

Lists

Lists are a fundamental data structure in Scheme, used to represent ordered collections of elements. Lists can contain elements of heterogeneous types, including numbers, strings, symbols, and even other lists.

Lists in Scheme are implemented as linked chains of pairs. Each pair’s car contains an element, and its cdr points to the next pair (or the empty list).

(cons 1 (cons 2 (cons 3 '())))  ; This creates the list (1 2 3)
'(1 . (2 . (3 . ())))           ; Pair notation for the same list
(list 1 2 3)                    ; Procedure calling for the same list
'(1 2 3)                        ; Quoted literal for the same list

The () notation represents the empty list, also known as nil.

By default list are immutable.

It is possible to concatenate cdr and car of a list to get an element of a list:

(car (cdr '(1 2 3))) ; This will return 2
(cadr '(1 2 3)) ; This is a shorthand for the previous expression, also returns 2

(car (cdr (cdr '(1 2 3)))) ; This will return 3
(caddr '(1 2 3)) ; This is a shorthand for the previous expression, also returns 3
Structs

Scheme allows the definition of custom data structures using the struct construct. They are also called records. This enables the creation of new types with named fields. By default, fields are immutable, but is possible to create mutable fields by specifying the #:mutable keyword.

(struct person (
  (name) 
  (age #:mutable)
))

A struct is instantiated by calling it as a procedure:

(define alice (person "Alice" 30))

To access the fields of a struct, accessor procedures are automatically generated:

(person-name alice) ; Returns "Alice"
(person-age alice)  ; Returns 30

To modify a mutable field, a setter procedure is also generated:

(set-person-age! alice 31) ; Updates Alice's age to 31

Structs can also support inheritance by specifying a parent struct:

(struct employee person (
  (employee-id)
))

Records are not classes and do not support methods or encapsulation like in object-oriented programming.

Procedures

In Scheme, procedures (or functions) are first-class citizens, meaning they can be treated like any other data type.

The parameters are passed by value (technically object sharing as objects are not copied in the stack but only the reference to the object is passed), meaning that the actual values of the arguments are passed to the procedure, rather than references to the variables.

Lambda Expressions

Lambda expressions are used to define anonymous functions in Scheme. Anonymous functions are functions that are defined without a name and can be used as arguments to other functions or assigned to variables. They are a fundamental concept in functional programming.

The syntax for a lambda expression is defined by the lambda or λ keyword and is as follows:

(lambda (arg1 arg2 ... argn) body)

Where arg1 to argn are the parameters of the function, and body is the expression that defines the function’s behavior.

For example, the following lambda expression defines a function that takes two arguments and returns their sum:

(lambda (x y) (+ x y))

Defining Named Procedures

Named procedures can be defined using the define keyword.

(define add
  (lambda (x y)
    (+ x y)))

Or using the syntactic sugar for procedure definition:

(define (add x y)
  (+ x y))

To define a procedure with a variable number of arguments (variadic), we can use the dot (.) notation:

(define (sum . numbers)
  (if (null? numbers)
      0
      (+ (car numbers) (apply sum (cdr numbers)))))

String Operations

Scheme provides several built-in procedures for manipulating strings:

Vector Operations

Scheme provides several built-in procedures for manipulating vectors:

List Operations

Scheme provides several built-in procedures for manipulating lists:

Then there are higher-order procedures that operate on lists like:

Syntactic Form

Scheme has some special syntactic forms that are not evaluated in the same way as regular expressions.

Conditional Branching

Conditional branching is used to evaluate different expressions based on certain conditions.

If Expression

The if expression is used for conditional branching in Scheme. In this expression, a condition is evaluated, and based on its truth value, one of two branches is executed, not both.

It has the following syntax:

(if condition then-branch else-branch)

Where condition is an expression that evaluates to a boolean value, then-branch is the expression executed if the condition is true, and else-branch is the expression executed if the condition is false.

When Expression

The when expression is a simplified form of conditional branching that only includes a then-branch. It is used when there is no need for an else-branch.

(when condition then-branch)
Cond Expression

The cond expression is used for multi-way branching, similar to switch or if-else if-else in other languages.

(cond
  (condition1 result1)
  (condition2 result2)
  (else result_else))
Case Expression

The case expression is used for branching based on the value of a single expression. It compares the value against multiple cases and executes the corresponding result for the first matching case.

(case expression
  ((value1 value2 ...) result1)
  ((value3 value4 ...) result2)
  (else result_else))
Unless Expression

The unless expression is the opposite of the when expression. It executes the then-branch only if the condition evaluates to false.

(unless condition then-branch)

Quote

The quote syntactic form is used to prevent the evaluation of an expression. When an expression is quoted, it is treated as a literal value rather than being evaluated.

The syntax for quoting an expression is as follows:

(quote expression)

Alternatively, a shorthand notation using a single apostrophe (') can be used:

'expression

For example, the expression '(1 2 3) represents the list containing the elements 1, 2, and 3 without evaluating it.

Quasiquote

Quasiquote is a syntactic form that allows for partial evaluation of an expression. It is denoted by the backquote character (`). Within a quasiquoted expression, parts of the expression can be evaluated using the comma (,) operator.

For example:

`(1 2 ,(+ 3 4)) ; This will evaluate to the list (1 2 7)
Eval

To evaluate a quoted expression at runtime, the eval function can be used:

(eval '(+ 1 2)) ; Returns 3

Procedural Code Execution

To write procedural code in Scheme, we can use the begin syntactic form, which allows for the sequential execution of multiple expressions.

The syntax for the begin form is as follows:

(begin expression1 expression2 ... expressionN)

Where expression1 to expressionN are the expressions to be executed in sequence. The value of the begin expression is the value of the last expression executed.

Predicates

Predicates are procedures that return a boolean value (#t for true and #f for false).

Type Predicates

Some common predicates in Scheme include checking the type of a value:

Equality Predicates

To compare two values for equality, Scheme provides several predicates:

Logical Operators

The logical operators and, or, and not can be used to combine predicates:

(and (number? x) (> x 0)) ; Checks if x is a positive number
(or (null? lst) (pair? lst)) ; Checks if lst is empty or a pair
(not (string? y)) ; Checks if y is not a string

Custom Predicates

Custom predicates can be defined using lambda expressions or named procedures that should have names ending with a question mark (?):

(define (even? n)
  (= (modulo n 2) 0))

Iteration

Scheme does not have traditional looping constructs like for or while found in imperative programming languages. Instead, iteration is typically achieved through recursion or named let expressions.

Named Let

Named let is a syntactic form that allows for defining recursive functions in a concise manner. The syntax for named let is as follows:

(let label ((param1 init1)
            (param2 init2)
            ...
            (paramN initN))
  body)

Where label is the name of block, param1 to paramN are the parameters with their initial values, and body is the expression that defines the function’s behavior.

To perform iteration, the label needs to be called to perform a goto-like jump to the beginning of the block with updated parameters.

(let label ((n 5))
  (when (>= n 0)
    (displayln n)
    (label (- n 1))))

Tail Recursion

Recursion is a fundamental concept in Scheme and functional programming in general. It involves defining a function that calls itself to solve a problem by breaking it down into smaller subproblems.

A special case of recursion is tail recursion, where the recursive call is the last operation in the function. Tail-recursive functions are optimized by the Scheme interpreter to execute in constant stack space, effectively turning them into iterations.

(define (factorial n acc)
  (if (= n 0)
      acc
      (factorial (- n 1) (* n acc)))) ; Tail call

(factorial 5 1) ; Evaluates to 120

To evaluate memory usage of tail-recursive functions, we can use the trace procedure to monitor function calls:

(require racket/trace) ; Import the trace module
(trace factorial)
(factorial 5 1)

Closure

A closure is a function that captures the lexical scope in which it was defined, allowing it to access variables from that scope even when invoked outside of it. It is made of two components:

(define (make-counter)
  (let ((count 0))
    (lambda ()
      (set! count (+ count 1))
      count)))

(define counter (make-counter))

(counter) ; Returns 1
(counter) ; Returns 2

This allows to create iterators and generators that maintain state across invocations.

The react’s useState hook is an example of closures in action, where the state variable and its updater function are captured within the closure created by the hook.

Macros

Macros in Scheme are a powerful feature that allows programmers to define new syntactic constructs and transformations at compile time. They enable the creation of custom language features by manipulating the code structure before its evaluation. This system is turing complete and allows for advanced metaprogramming techniques that are evaluated at compile time.

Macro definitions use the define-syntax and syntax-rules constructs.

(define-syntax while
  (syntax-rules ()
    ((_ condition body ...)
      (let loop ()
        (when condition
          (begin
            body ...
            (loop))
        ))
    )
  ))

where:

(define-syntax let
  (syntax-rules ()
    ((_ ((var expr) ...) body ...)
      ((lambda (var ...) body ...) expr ...))
  ))

This macro transforms a let expression into an equivalent lambda application, (var expr) ... represent multiple variable bindings.

syntax-rules can also define literals, which are identifiers that must match exactly during macro expansion and are not treated as pattern variables.

(define-syntax my-if
  (syntax-rules (else) ; 'else' is a literal keyword
    ((_ condition then-branch else else-branch)
      (if condition then-branch else-branch))))

In this example, else is a literal—it must appear verbatim in the macro call. If you write (my-if (> x 0) 1 else 2), the else keyword is recognized. Using a different identifier would not match the pattern.

Hygienic Macros

Macros in sceme are hygienic meaning that they prevents unintended variable capture and name collisions during macro expansion. Unlike traditional macros that operate on raw text substitution, hygienic macros maintain the lexical scope of identifiers, ensuring that macro parameters and locally-bound variables don’t inadvertently conflict with variables in the scope where the macro is invoked. This is achieved through automatic renaming of identifiers during expansion, typically by attaching unique tags or timestamps to variable names.

To use global variable, defined at the top level, the name must be surrounded by * symbols, for example (define *global-var* '()).

To break hygiene and allow variable capture, the user should pass the variable as a parameter to the macro:

(define-syntax with-temp
  (syntax-rules ()
    ((_ var body ...)
      (let ((var (make-temp)))
        body ...))))

Continuations

Continuations represent “the rest of the computation” at any given point in a program. They capture what the program will do next, allowing you to pause execution, save that state, and resume it later, possibly multiple times or from different contexts.

In Scheme, continuations are accessed using call/cc (call-with-current-continuation), a procedure that captures the current continuation and passes it to a function as an argument.

Saving Continuations for Later

Continuations become powerful when you save them and invoke them later:

(define saved-k #f)

(+ 1 
  (call/cc
    (lambda (k)
      (set! saved-k k)
      10))) ; Returns 11

(saved-k 20) ; Returns 21

When calling (saved-k 20), you’re saying: “resume the computation from where I captured this continuation, but use 20 instead of 10.” This effectively re-executes (+ 1 20), returning 21.

Early Exit from Loops

A practical use case is breaking out of loops without checking all remaining elements:

(define (search lst target)
  (call/cc
    (lambda (exit)
      (for-each
        (lambda (x)
          (if (= x target)
              (exit x)))
        lst)
      #f)))

(search '(1 2 3 4 5) 3) ; Returns 3

When the target is found, calling exit abandons the rest of the loop and returns the result immediately.

Goto-like jumps

Continuations can also be used to implement goto-like jumps in the code, allowing for non-linear control flow.

(let ([cc (call/cc (lambda (k) (k k)))])
  (set! x (add1 x))
  (if (< x 3)
    (cc cc)   ; Jump back to the beginning
    x
  )
)

Implementation

Continuations can be implemented using two main techniques:

Non-Deterministic Programming

Scheme supports non-deterministic programming through the use of continuations, allowing for the exploration of multiple computation paths.

Using the choose construct, we can define non-deterministic choices in our programs. This construct allows us to specify a set of possible values, and the program can explore different paths based on these choices. This construct stores the current continuation before making a choice, enabling backtracking if needed.

If at any point the we understand that the current computation path is not leading to a valid solution, we can use the fail procedure to backtrack and explore alternative paths. This construct will restore the last saved continuation, removing the current computation state and allowing the program to try a different choice.

(define (is-the-sum-of sum)
  (unless (and (>= sum 0)(<= sum 10))
    (error "out of range" sum))
  (let ((x (choose ’(0 1 2 3 4 5)))
        (y (choose ’(0 1 2 3 4 5))))
    (if (= (+ x y) sum)
      (list x y)
      (fail)
    )
  )
)

This function create two non-deterministic variables x and y. At the beginning they are both 0. When the fail procedure is called, the last continuation is restored, and y becomes 1. If fail is called again, y becomes 2, and so on. When all values for y are exhausted the y choice will fail and x will be incremented to 1, restarting the cycle for y.

Proto-OO

Scheme does not have built-in support for Object Oriented Programming (OOP) like class-based languages (e.g., Java, C++). However, it is possible to implement OOP concepts using various patterns, such as struct-based implementation, message passing, and prototype-based implementation.

Object Oriented Programming (OOP) is a programming paradigm based on the concept of “objects”, which can contain data and code: data in the form of fields (often known as attributes or properties), and code in the form of procedures (often known as methods).

Struct-based Implementation (Records)

Closures allow for data encapsulation as the data can be hidden within the closure’s environment.

By storing lambdas inside a struct (or record), we can create objects where data is hidden within the closure’s environment.

(struct counter (inc dec val))

(define (make-counter)
  (let ((count 0))                         ; internal state
    (counter
      (lambda () (set! count (+ count 1))) ; inc
      (lambda () (set! count (- count 1))) ; dec
      (lambda () count))))                 ; val

(define c (make-counter))

((counter-inc c))   ; Increments count
((counter-val c))   ; Returns 1

In this pattern, the “object” is a record of functions, and “methods” are invoked by selecting a field and calling the returned lambda.

Message Passing Implementation

In the message passing pattern, an object is represented as a single procedure (the dispatcher).

This procedure accepts a “message” (usually a symbol) and arguments, then decides which internal logic to execute.

This follows Alan Kay’s original vision of OOP, where communication between objects happens through messages rather than direct method calls.

(define (make-counter)
  (let ((count 0))
    ;; Local methods
    (define (inc)         (set! count (+ count 1)))
    (define (set-val val) (set! count val))
    (define (get-val)     count)
  
    ;; The Dispatcher
    (lambda (message . args)
      (case message
        ((increase) (apply inc args))
        ((set)      (apply set-val args))
        ((value)    (apply get-val args))
        (else       (error "Unknown message" message))))))

(define c (make-counter))
(c 'increase) ; Increments
(c 'value)    ; Returns 1
Delegation and Inheritance

Inheritance is achieved by delegation: if the child object doesn’t recognize a message, it passes it to a “parent” object.

(define (make-dec-counter)
  (let ((parent (make-counter)))
    (define (decrease) (parent 'set (sub1 (parent 'value))))
    (define (increase) (parent 'set (add1 (parent 'value))))
  
    (lambda (message . args)
      (case message
        ((decrease) (apply decrease args))        ; new methods
        ((increase) (apply increase args))        ; override parent methods
        (else (apply parent (cons message args))) ; delegate to parent
      )
    )
  )
)

Prototype-based Implementation

In prototype-based OO (like JavaScript or Lua), there are no classes. Objects are cloned from existing prototypes, and behaviors are added or modified dynamically.

Hash tables are typically used to store fields and methods.

(define new-object make-hash)
(define clone      hash-copy)

;; Macros for cleaner syntax
(define-syntax !!
  (syntax-rules ()
    ((_ obj key val)
      (hash-set! obj 'key val))))
(define-syntax ??
  (syntax-rules ()
    ((_ obj key)
      (hash-ref obj 'key))))
(define-syntax ->
  (syntax-rules ()
    ((_ obj msg arg ...)
      ((?? obj msg) obj arg ...))))

(define person (new-object))
(!! person name "Alice")
(!! person greet
    (lambda (self) (string-append "Hi, I'm " (?? self name))))

(-> person greet) ; "Hi, I'm Alice"

The self argument must be passed explicitly to methods to allow access to the object’s local state, mimicking the this pointer.

Inheritance via Prototype Chain

Inheritance is implemented via a dispatch chain: if a property is not found in the current object, the search continues in its parent (prototype).

(define (deep-clone obj)
  (if (not (hash-ref obj '<<parent>> #f))
    (clone obj)
    (let* ((cl (clone obj))
          (par (?? cl <<parent>>)))
      (!! cl <<parent>> (deep-clone par)))))
      
(define (son-of parent)
  (let ((o (new-object)))
    (!! o <<parent>> (deep-clone parent))
    o))

(define (dispatch obj msg)
  (if (eq? obj 'unknown)
    (error "Unknown message" msg)
    (let ((slot (hash-ref obj msg 'unknown)))
      (if (eq? slot 'unknown)
        (dispatch (hash-ref obj '<<parent>> 'unknown) msg)
        slot))))

(define-syntax ??       ;; reader
  (syntax-rules ()
    ((_ obj key)
      (dispatch obj 'key))))
(define-syntax ->        ;; send message
  (syntax-rules ()
    ((_ obj msg arg ...)
      ((dispatch obj 'msg) obj arg ...))))

Error Handling

In scheme errors are raised using the error procedure:

(error "An error occurred")

To define a custom error handling mechanism we must:

Haskell

Haskell is a purely functional programming language based on immutability and side-effect-free functions. It is statically typed with strong type inference and polymorphism.

Evaluation Strategy

Being purely functional allows the order of evaluation to be irrelevant, enabling lazy evaluation of redexes (reducible expressions). This means that expressions are not evaluated until their values are actually needed, which:

A redex is an expression that can be reduced or simplified according to the rules of the language. In functional programming, a redex typically refers to a function application that can be evaluated to produce a result.

Call by Need

When calling a function, Haskell uses call by need evaluation strategy, which is an optimization of call by name. In call by need:

  1. Arguments to a function are not evaluated until they are actually used
  2. Once evaluated, the result is cached (“memoized”)
  3. Subsequent uses of the same argument reuse the cached value without re-evaluation

This is implemented using thunks, deferred computations. A thunk is a parameterless function that encapsulates an expression to be evaluated later.

Evaluation Strategies Comparison:

Strategy Order Description
Call by Value Innermost-first Evaluates arguments before function application (leftmost redexes first)
Call by Name Outermost-first Evaluates function application before arguments (outermost redexes first)
Call by Need Outermost-first + memoization Like call by name, but caches results

Why Call by Name is More Robust:

Call by name is more robust than call by value (Church-Rosser confluence) because it guarantees finding a normal form (fully reduced expression) when one exists. This is because:

Scheme Implementation of Call by Name

In Scheme, we can simulate call by name using lambda expressions to create promises (thunks):

(struct promise (thunk value?) #:mutable)

(define-syntax delay
  (syntax-rules ()
    ((_ expr ...)
      (promise (lambda () expr ...) #f))))

(define (force p)
  (cond
    ((not (promise? p)) p)                    ; If not a promise, return as is
    ((promise-value? p) (promise-value? p))   ; If already evaluated, return cached value
    (else                                     ; Otherwise, evaluate the thunk
      (let ((val ((promise-thunk p))))
        (set-promise-value?! p val)           ; Cache the evaluated value
        val))))

Forcing Strict Evaluation

Haskell provides mechanisms to override lazy evaluation when needed:

  1. BangPatterns (! symbol): Indicates that a value should be evaluated immediately (eagerly) rather than lazily.

    factorial :: Int -> Int
    factorial 0 = 1
    factorial n = n * factorial (n - 1)  -- Normal lazy evaluation
    
    factorial' :: !Int -> Int
    factorial' 0 = 1
    factorial' !n = n * factorial' (n - 1)  -- Eager evaluation
  2. The seq function: Forces evaluation of an expression before proceeding with the rest of the computation.

    factorial'' :: Int -> Int
    factorial'' 0 = 1
    factorial'' n = n `seq` (n * factorial'' (n - 1))  -- Force evaluation of n

Use Case: Strict evaluation is useful for preventing space leaks in accumulating parameters and improving performance in specific scenarios.

Variables

Variables in Haskell are immutable by default, once a variable is bound to a value, it cannot be changed. This immutability is a core principle of functional programming and ensures referential transparency (an expression can be replaced by its value without changing program behavior).

Defining Variables

Haskell provides two keywords for defining local variables: let and where.

The let keyword allows us to define local variables within an expression:

square :: Int -> Int
square x = 
  let y = x * x 
  in y

The where keyword allows us to define local variables at the end of a function definition that will be resolved as substitutions in the function body:

square :: Int -> Int
square x = y
  where y = x * x

Difference: let is an expression (can be used anywhere), while where is a declaration (only at function level).

Type System

Haskell is a statically typed language — the type of every expression is known at compile time.

Type Inference

Haskell uses type inference via the Hindley-Milner type system to automatically deduce types without explicit annotations. The system infers the most general type for each expression.

Type Annotations:

The type can be explicitly specified using the :: operator:

x :: Int
x = 42

Built-in Types

Haskell provides several primitive types:

Type Aliases

Create descriptive names for existing types using the type keyword:

type String = [Char]

User-Defined Types

Haskell allows creating custom data types using the data keyword.

data Shape = Circle Float | Rectangle Float Float

To create instances of user-defined types, we use the data constructors:

circle :: Shape
circle = Circle 5.0

rectangle :: Shape
rectangle = Rectangle 4.0 6.0

Parametric Types (Generics)

Type constructors can take type parameters:

data Maybe a = Nothing | Just a  -- 'a' is a type variable

Accessing Fields

Positional Access (Pattern Matching)

By default, fields are accessed via pattern matching:

data Point = Point Float Float

point :: Point
point = Point 3.0 4.0

pointX :: Point -> Float
pointX (Point x _) = x  -- Extracts the first field

To simplify access, custom accessor functions can be defined manually:

getX :: Point -> Float
getX (Point x _) = x

xVal :: Float
xVal = getX point  -- Accessing the x field
Record Syntax (Named Fields)

Record syntax provides named fields and automatically generates accessor functions:

data Point = Point { x :: Float, y :: Float }

point :: Point
point = Point { x = 3.0, y = 4.0 }

valX :: Float
valX = x point  -- 'x' is now a function: Point -> Float

Type Classes

Type classes define a set of functions that can operate on different types, providing polymorphism (similar to interfaces in OOP languages). Any type can become an instance of a type class by implementing its required methods.

Defining Type Classes

Use the class keyword to define a type class:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

The type signature (==) :: (Eq a) => a -> a -> Bool means:

Creating Instances

Use the instance keyword to make a type an instance of a type class:

instance Eq Bool where
  True  == True  = True
  False == False = True
  _     == _     = False

Using Type Constraints

Enforce type class constraints in a signatures:

-- Single constraint
areEqual :: (Eq a) => a -> a -> Bool
areEqual x y = x == y

-- Multiple constraints (comma-separated)
showAndEq :: (Show a, Eq a) => a -> a -> String
showAndEq x y = "Are they equal? " ++ show (x == y)

Common Type Classes

Eq

Supports equality testing with two methods:

class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
Ord

Supports ordering and comparison operations:

class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<)     :: a -> a -> Bool
  (<=)    :: a -> a -> Bool
  (>)     :: a -> a -> Bool
  (>=)    :: a -> a -> Bool

Note: Eq a => Ord a indicates that Ord requires Eq, any ordered type must also support equality.

Foldable

For data structures that can be reduced (folded) to a summary value:

class Foldable t where
  foldr :: (a -> b -> b) -> b -> t a -> b

  foldl :: (b -> a -> b) -> b -> t a -> b
  foldl f a bs = foldr (\b g x -> g (f x b)) id bs a

-- example

instance Foldable Maybe where
  foldr _ z Nothing  = z
  foldr f z (Just x) = f x z

instance Foldable List where
  foldr _ z []     = z
  foldr f z (x:xs) = f x (foldr f z xs)

If the data structure has multiple parameters all the parameters, except for the last one, are fixed, and the last one is the target of the computation:

data Llist b a

instance Foldable (Llist b) where
    foldr _ z Nul           = z
    foldr f z (Nod a b xs)  = f a (foldr f z xs)
Functor

For types that can be mapped over (containers that support applying a function to their contents):

class Functor f where
  fmap :: (a -> b) -> f a -> f b

-- example

instance Functor Maybe where
  fmap _ Nothing  = Nothing
  fmap f (Just x) = Just (f x)

instance Functor List where
  fmap _ []     = []
  fmap f (x:xs) = f x : fmap f xs

Functor Laws:

All Functor instances must satisfy:

  1. Identity: fmap id = id (mapping the identity function does nothing)
  2. Composition: fmap (f . g) = fmap f . fmap g (mapping a composition equals composing the maps)
Applicative

Extends Functor to support applying functions wrapped in a context to values wrapped in a context:

class Functor f => Applicative f where
  pure :: a -> f a
  (<*>) :: f (a -> b) -> f a -> f b

-- example

instance Applicative Maybe where
  pure = Just
  Nothing <*> _ = Nothing
  _ <*> Nothing = Nothing
  (Just f) <*> (Just x) = Just (f x)

instance Applicative List where
  pure x = [x]
  fs <*> xs = [f x | f <- fs, x <- xs]

Where:

Monad

Extends Applicative to represent computations (called actions) that can be sequenced, where each computation can depend on the result of the previous one.

Monads enable imperative-style programming in a functional language while maintaining purity.

Methods:

class Applicative m => Monad m where
  (>>=)  :: m a -> (a -> m b) -> m b

  (>>)   :: m a -> m b -> m b
  m >> k = m >>= \_ -> k

  return :: a -> m a
  return = pure

-- example

instance Monad Maybe where
  Nothing >>= _ = Nothing
  (Just x) >>= f = f x

instance Monad List where
  [] >>= _ = []
  (x:xs) >>= f = f x ++ (xs >>= f)

Monad Laws:

All Monad instances must satisfy:

  1. Left Identity: return a >>= f == f a
  2. Right Identity: m >>= return == m
  3. Associativity: (m >>= f) >>= g == m >>= (\x -> f x >>= g)
Do Notation

Provides syntactic sugar for chaining monadic operations, making monadic code more readable and imperative-looking.

example :: Maybe Int
example = do
  x <- Just 3
  y <- Just 4
  return (x + y)

Desugared version:

example :: Maybe Int
example = Just 3 >>= \x ->
            Just 4 >>= \y ->
              return (x + y)

List comprehensions are syntactic sugar for the list monad:

-- These three are equivalent:
do x <- [1, 2]
   y <- [3, 4]
   return (x, y)

[1, 2] >>= \x -> [3, 4] >>= \y -> return (x, y)

[(x, y) | x <- [1, 2], y <- [3, 4]]

-- All produce: [(1,3), (1,4), (2,3), (2,4)]
State Monad

Encapsulates stateful computations, allowing functions to read and modify shared state without explicit parameter passing.

Type: State s a represents a computation that:

data State s a = State ( s -> (s, a) )

instance Functor (State s) where
  fmap f (State g) = State $ \s ->
    let (s', x) = g s
    in (s', f x)

instance Applicative (State s) where
  pure x = State $ \s -> (s, x)

  (State f) <*> (State g) = State $ \s ->
    let (s', h) = f s
        (s'', x) = g s'
    in (s'', h x)

instance Monad (State s) where
  (State g) >>= f = State $ \s ->
    let (s', x) = g s
        (State h) = f x
    in h s'

-- runState is used to execute a State computation with an initial state
runState :: State s a -> s -> (s, a)
runState (State f) s = f s

-- Example usage of State monad
get :: State s s
get = State $ \s -> (s, s)

put :: s -> State s ()
put newState = State $ \_ -> (newState, ())

modify :: (s -> s) -> State s ()
modify f = do
  s <- get
  put (f s)

main :: IO ()
main = do
    let (finalState, result) = runState 
          (do x <- get          -- Get the state (initially 100)
              modify (+ 10)     -- Modify the state by adding 10 (state becomes 110)
              return (x + 1))   -- Return the result of adding 1 to the original state (100)
          100
    
    print result                -- Print the result (101)
    print finalState            -- Print the final state (110)

Key Functions:

Data Structures

Haskell provides several built-in immutable data structures:

Arrays

Fixed-size collections (Data.Array module):

Maps

Key-value pairs (Data.Map module):

Functions

Haskell functions are first-class citizens, they can be:

Basic Function Definition

Use the -> operator in type signatures:

add1 :: Int -> Int
add1 x = x + 1

A function is called by simply providing its arguments:

result :: Int
result = add1 5  -- result will be 6

Point-Free Style

Functions can be defined without explicitly mentioning arguments:

add1 :: Int -> Int
add1 = (+ 1)  -- Partial application of (+)

Infix Operators

Haskell allows functions with two arguments to be used as infix operators by enclosing their names in backticks (`).

5 `add` 3  -- Equivalent to (add 5 3)

Conversely, an infix operator (non-alphabetical like + or *) can be used as a prefix function by enclosing it in parentheses ().

(+) 5 3  -- Equivalent to (5 + 3)

Lambda Functions

Lambda functions are defined using the \ symbol followed by the parameters, an arrow ->, and the function body.

\x y -> x + y + 1

Composition

Function composition is achieved using the (.) operator, which allows combining two functions into a new function.

(.) :: (b -> c) -> (a -> b) -> a -> c
(.) f g x = f (g x)

-- Example usage:
increment :: Int -> Int
increment = (+1)
double :: Int -> Int
double = (*2)

incrementThenDouble :: Int -> Int
incrementThenDouble = double . increment

result :: Int
result = incrementThenDouble 3  -- result will be 8

Function Application Operator

The $ operator is used for function application and can be used to avoid parentheses.

result :: Int
result = double $ increment 3  -- Equivalent to double (increment 3)

Currying

All functions in Haskell are curried by default, meaning that a multi-argument function is actually a chain of single-argument functions.

Mathematical representation:

A function f: \mathbb{N} \times \mathbb{Z} \rightarrow \mathbb{Q} is represented as:

f: \mathbb{N} \rightarrow (\mathbb{Z} \rightarrow \mathbb{Q})

Or simply: f: \mathbb{N} \rightarrow \mathbb{Z} \rightarrow \mathbb{Q} (since \rightarrow is right-associative)

add :: Integer -> Integer -> Integer
add x y = x + y

The function add takes an integer x and returns a new function that takes an integer y and returns the sum of x and y.

Partial Application

Currying enables partial application of functions, meaning that providing only some arguments returns a new function expecting the remaining ones:

increment :: Int -> Int
increment = add 1

The increment function is created by partially applying the add function with the first argument set to 1. It takes a single integer argument and adds 1 to it.

Polymorphism

Haskell supports parametric polymorphism, allowing functions to operate on values of any type without being tied to a specific one. This is achieved using type variables.

identity :: a -> a
identity x = x

The identity function takes a value of any type a and returns a value of the same type.

This is similar to generics in languages like Java or C#.

Pattern Matching

In Haskell, pattern matching is a powerful feature that allows to define functions by specifying patterns for their arguments. When a function is called, Haskell tries to match the provided arguments against the defined patterns in order from top to bottom.

factorial :: Integer -> Integer
factorial 0 = 1
factorial n = n * factorial (n - 1)

Haskell also support boolean guards to define conditions for different cases in function definitions.

absolute :: Int -> Int
absolute x
  | x < 0     = -x
  | otherwise = x

It is possible to define a function using the case expression, which allows for pattern matching within the function body.

describeList :: [a] -> String
describeList xs = case xs of
  []      -> "The list is empty."
  [x]     -> "The list has one element."
  (x:y:_) -> "The list has multiple elements."

Common Higher-Order Functions

Haskell’s standard library provides powerful functions for list manipulation:

Infinite Data Structures

Haskell’s lazy evaluation enables working with infinite data structures. Values are computed only when needed, allowing theoretically infinite lists.

-- Infinite list of ones
ones :: [Int]
ones = 1 : ones

-- Infinite list from n onward  
ones' = [1, 1..]  -- Alternative notation

numFrom n = n : numFrom (n+1) -- An infinite list of natural numbers starting from n
squares = map (^2) (numFrom 1) -- An infinite list of squares of natural numbers

We can work with infinite lists using functions like take, which retrieves a finite number of elements from the beginning of the list.

firstFiveOnes :: [Int]
firstFiveOnes = take 5 ones  -- Returns [1, 1, 1, 1, 1]

Control Flow

Conditional Expressions

Haskell provides if-then-else expressions:

if condition then trueExpression else falseExpression

The else clause is mandatory (since expressions must always return a value).

Implementation as a function:

if' :: Bool -> a -> a -> a
if' True  x _ = x
if' False _ y = y

Haskell Iteration

Haskell uses recursion with pattern matching instead of traditional loops:

Input/Output

Haskell handles I/O using the IO monad, which encapsulates side-effecting operations while maintaining functional purity.

main :: IO ()
main = do
  putStrLn "Hello, World!"

Note: The IO monad internally uses an implicit “world state” token to sequence operations.

Basic I/O Operations

Command Line Arguments

Haskell provides the System.Environment module to access command line arguments.

import System.Environment

main :: IO ()
main = do
  args <- getArgs
  putStrLn ("Command line arguments: " ++ show args)

  progName <- getProgName
  putStrLn ("Program name: " ++ progName)

Haskell Error Handling

Bottom Type (⊥)

Represents non-terminating computations and errors. Defined as ⊥ = ⊥ or bot = bot.

Raising errors

error :: String -> a
error "An error occurred"  -- Terminates program with error message

Safe Error Handling with Maybe

The Maybe type represents computations that may fail:

data Maybe a = Nothing | Just a

Exception Handling in IO

The Control.Exception module provides exception handling for IO operations. This is done with the handle function, which takes an exception handler and an IO action.

import Control.Exception
main :: IO ()
main = handle handler (do
    putStrLn "Enter a number:"
    input <- getLine
    let number = read input :: Int
    print (10 `div` number))
  where
    handler :: SomeException -> IO ()
    handler ex = putStrLn $ "Caught an exception: " ++ show ex

Erlang

Erlang is a functional programming language designed for building concurrent, distributed, and fault-tolerant systems.

Erlang’s actor model inspired the Akka framework (Scala/Java) and influenced the “Reactive Manifesto” for building responsive systems.

Erlang Variables

In Erlang, variables are immutable. Once a variable is bound to a value, it cannot be changed. Attempting to rebind a variable results in a runtime error.

Naming Convention:

X = 10         % Bind X to 10
Y = X + 5      % Bind Y to 15
_ = foo()      % Call foo() but ignore the result

Data Types

Erlang provides several built-in data types:

Atoms

Atoms are constants whose value is their own name (similar to symbols in other languages).

Syntax:

ok                        % Atom
error                     % Atom  
'Error occurred'          % Atom with spaces
'Hello@World!'            % Atom with special characters

Atoms are stored in a global atom table (not garbage collected) and are extremely fast to compare (just comparing references)

Tuples

Fixed-size collections of elements, defined using curly braces {}.

Point = {3, 4}                    % A tuple representing a point (x, y)
Person = {person, "Alice", 30}    % Common pattern: {tag, ...data}

Common Functions:

Common Pattern: Use tuples with an atom tag as the first element: {ok, Result}, {error, Reason}

Erlang Lists

Ordered, variable-length collections, defined using square brackets [].

Numbers = [1, 2, 3, 4, 5]         % A list of numbers
Mixed = [atom, 42, "string"]      % Lists can contain mixed types
Empty = []                         % Empty list

List Operations:

List1 = [1, 2, 3].
List2 = [4, 5, 6].

% Concatenation
List1 ++ List2.      % Results in [1, 2, 3, 4, 5, 6]

% Cons operator |
[0 | List1].         % Results in [0, 1, 2, 3]
[H | T] = [1, 2, 3]. % Pattern matching: H=1, T=[2,3]

% Warning: This creates a nested list!
[List1 | List2].     % Results in [[1, 2, 3], 4, 5, 6]

Common Functions:

List Comprehensions

Concise syntax for generating lists:

Squares = [X * X || X <- [1, 2, 3, 4, 5]].              % [1, 4, 9, 16, 25]
Evens = [X || X <- [1, 2, 3, 4, 5], X rem 2 == 0].      % [2, 4]
Pairs = [{X, Y} || X <- [1, 2], Y <- [a, b]].           % [{1,a}, {1,b}, {2,a}, {2,b}]

Erlang Maps

Key-value pairs (hash maps), defined using #{} syntax.

Syntax:

% Creating maps
Person = #{name => "Alice", age => 30}.
Empty = #{}.

% Accessing values
Name = maps:get(name, Person).     % Returns "Alice" or throws exception
Age = maps:get(age, Person, 0).    % Returns 30, or 0 if not found

% Updating (creates new map)
Updated = Person#{age => 31}.      % Update existing key
Updated2 = Person#{city => "NYC"}. % Add new key
Updated3 = Person#{age := 31}.     % := requires key to exist

Common Functions:

Erlang Pattern Matching

Erlang uses pattern matching as a fundamental feature for:

Basic Examples:

% Tuple matching
{A, B} = {10, 20}.                % A=10, B=20
{ok, Value} = {ok, 42}.           % Value=42
{A, A, B} = {1, 1, 2}.            % A=1, B=2 (A must match both positions)
{A, A, B} = {1, 2, 3}.            % ERROR: A can't be both 1 and 2

% List matching
[A, B | Rest] = [1, 2, 3, 4, 5].  % A=1, B=2, Rest=[3,4,5]

% Map matching
#{name := Name} = #{name => "Alice", age => 30}. % Name="Alice"

Erlang Functions

Functions are defined with multiple clauses, selected by pattern matching on arguments.

Syntax Rules:

factorial(0) -> 1;                    % Base case
factorial(N) -> N * factorial(N - 1). % Recursive case

Function Arity

Functions are identified by name/arity (name and number of arguments).

The same name can be used for different arities:

add(X, Y) -> X + Y.           % add/2
add(X, Y, Z) -> X + Y + Z.    % add/3 (different function)

Referencing Functions:

Use the / notation to specify the specific function:

Result = add(2, 3).           % Calls add/2
Result2 = add(1, 2, 3).       % Calls add/3
Fun = fun add/2.              % Reference to add/2 function

Anonymous Functions (Lambdas)

Defined using the fun keyword:

% Simple lambda
Add = fun(X, Y) -> X + Y end.
Result = Add(2, 3).  % Result is 5

% Lambda with multiple clauses
Abs = fun(X) when X < 0 -> -X;
         (X) -> X
      end.

Note: Named functions must be referenced with the fun Name/Arity syntax when passed as arguments.

Guards

Additional conditions that refine pattern matching, specified with the when keyword.

absolute(X) when X < 0 -> -X;
absolute(X) -> X.

max(X, Y) when X > Y -> X;
max(_, Y) -> Y.

% Multiple guards with semicolon (OR)
is_valid(X) when X > 0; X < -10 -> true;
is_valid(_) -> false.

% Multiple conditions with comma (AND)
in_range(X) when X > 0, X < 100 -> true;
in_range(_) -> false.

Guards uses a specific sub-language to ensure validation in constant time. It allows:

Restriction: User-defined functions cannot be called in guards. Only built-in guard functions are allowed.

Modules

Erlang code is organized into modules, units of compilation and namespace.

Module Structure:

% Module declaration (must match filename: math_utils.erl)
-module(math_utils).

% Export public functions (name/arity)
-export([factorial/1, absolute/1]).

% Optional: Import functions from other modules
-import(lists, [map/2, filter/2]).

% Function definitions
factorial(0) -> 1;
factorial(N) -> N * factorial(N - 1).

absolute(X) when X < 0 -> -X;
absolute(X) -> X.

% Private function (not exported)
helper(X) -> X * 2.

Using Modules:

% Call exported function with Module:Function syntax
X = math_utils:factorial(5).  % 120

% Compile module
c(math_utils).  % In Erlang shell

Erlang Control Flow

If Expressions

The if expression uses guard expressions for conditions. Evaluates guards in order and executes the first true branch.

classify(X) ->
  if
    X < 0 -> negative;
    X > 0 -> positive;
    true -> zero    % Catch-all (like 'else')
  end.

Case Expressions

The case expression is based on pattern matching (more powerful than if as it allows calling custom functions).

% Basic case
case X of
  0 -> zero;
  1 -> one;
  _ -> other  % _ matches anything (catch-all)
end.

% Case with function calls
case lists:member(a, X) of
  true -> contains_a;
  false -> does_not_contain_a
end.

Iteration (Recursion)

Erlang uses recursion instead of loops. Use tail recursion for efficiency (constant stack space).

countdown(0) ->
  ok;
countdown(N) when N > 0 ->
  io:format("~p~n", [N]),
  countdown(N - 1).

Concurrency

Erlang is built on the Actor Model, everything runs in isolated processes that communicate via message passing.

Concurrency Models Comparison:

Core Primitives

  1. spawn - Create a process:

    Pid = spawn(Module, Function, Args).        % Returns process ID
    Pid = spawn(fun() -> loop() end).           % Spawn with lambda
    Pid = spawn(fun() -> Fucntion(Args) end)    % Safer way
  2. ! - Send a message (asynchronous):

    Pid ! {self(), hello}.  % Send message to Pid
    Pid ! stop.
  3. receive - Receive messages: Takes the first message from the mailbox that matches a pattern. Blocks if no match.

    receive
      {From, Msg} -> 
        io:format("Received ~p from ~p~n", [Msg, From]);
      stop -> 
        ok;
      _ -> 
        io:format("Unknown message~n")
    end.

Complete Example:

% Echo server: receives messages and sends responses
echo() ->
  receive
    {From, Message} ->
      io:format("Received: ~p~n", [Message]),
      From ! {self(), "Message received!"},  % Reply to sender
      echo();  % Tail recursive call to keep process alive
    stop ->
      io:format("Stopping echo server~n"),
      ok  % Terminate (process exits)
  end.

% Start the echo server
start() ->
  Pid = spawn(fun echo/0),              % Create new process
  Pid ! {self(), "Hello, Erlang!"},     % Send message
  receive
    {Pid, Reply} -> io:format("Got reply: ~p~n", [Reply])
  end,
  Pid ! stop.  % Stop the server

Useful Built-ins:

Registered Processes

Processes can be registered with global atom names for easier access (no need to track PIDs).

% Register a process
start() ->
  Pid = spawn(fun echo/0),
  register(echo_server, Pid),  % Register with atom name
  ok.

% Send message using registered name
echo_server ! {self(), "Hello"}.

Some useful functions are:

Encapsulation Pattern

A good practice when implementing modules is to export only public API functions keeping process implementation private.

-module(echo).
% Public API
-export([start/0, send_message/1, stop/0]).

% Public functions
start() ->
  Pid = spawn(fun loop/0),      % spawn internal function
  register(echo_server, Pid),
  {ok, Pid}.

send_message(Message) ->
  echo_server ! {self(), Message},
  ok.

stop() ->
  echo_server ! stop,
  ok.

% Private functions (not exported)
loop() ->
  receive
    {From, Msg} ->
      From ! {echo, Msg},
      loop();
    stop ->
      ok
  end.

Timeouts and Timers

Receive Timeout:

Handle cases where no matching message arrives within a time limit.

receive
  {data, Value} ->
    io:format("Received: ~p~n", [Value])
after 5000 ->  % Timeout after 5000 ms (5 seconds)
    io:format("No message received~n")
end.

% Timeout of 0: check mailbox without blocking
receive
  Msg -> handle(Msg)
after 0 ->
    no_message
end.

Delays:

timer:sleep(1000).  % Sleep current process for 1 second

Scheduled Messages:

% Timer with reference (can be cancelled)
Ref = erlang:start_timer(3000, self(), tick).
erlang:cancel_timer(Ref).  % Cancel if needed

Flush Mailbox

The flush() function clears all messages in the current process’s mailbox:

flush().

Error Handling Philosophy: “Let It Crash”

Erlang embraces a unique philosophy: “Let It Crash”.

Core Principles:

  1. Don’t defend against every possible error, as failures in distributed system are inevitable
  2. Isolate failures, one process crash doesn’t bring down the system
  3. Use supervisors, monitoring processes that restart failed workers
  4. Design for recovery, make restart cheap and state restorable

Supervisors

Supervisors are special processes that monitor workers and can restart them on failure.

A supervisor needs to be linked to a worker process. This allows to receive exit signals when the worker crashes, and stop the worker when the supervisor crashes.

To be able to handle exit signals, the supervisor process must set trap_exit flag to true. This converts exit signals into messages ({'EXIT', Pid, Reason}) that the supervisor can receive and handle.

% Start supervisor with N workers
start_supervisor(Count) ->
  process_flag(trap_exit, true),  % Convert exit signals to messages
  spawn_workers(Count),
  supervisor_loop(Count).

% Spawn N linked workers
spawn_workers(0) -> ok;
spawn_workers(N) -> 
  spawn_link(fun worker/0),  % Create linked worker
  spawn_workers(N - 1).

% Supervisor main loop
supervisor_loop(Count) ->
  receive
    {'EXIT', Pid, normal} ->
      % Worker exited normally
      io:format("Worker ~p exited normally~n", [Pid]),
      NewCount = Count - 1,
      if
        NewCount > 0 -> supervisor_loop(NewCount);
        true -> io:format("All workers done. Shutting down.~n")
      end;
    
    {'EXIT', Pid, Reason} ->
      % Worker crashed - restart it
      io:format("Worker ~p crashed (~p). Restarting...~n", [Pid, Reason]),
      spawn_workers(1),  % Restart one worker
      supervisor_loop(Count)  % Keep same count
  end.

% Worker process (may crash randomly)
worker() ->
  timer:sleep(1000),
  case rand:uniform(10) of
    N when N > 7 -> 
      exit(random_crash);  % 30% chance to crash
    _ -> 
      io:format("Worker ~p working...~n", [self()]),
      worker()  % Continue working
  end.

Erlang Patterns

Ultima modifica:
Scritto da: Andrea Lunghi